{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Copyright 2018 Google LLC  \n",
    "  \n",
    " Licensed under the Apache License, Version 2.0 (the \"License\");  \n",
    " you may not use this file except in compliance with the License.  \n",
    " You may obtain a copy of the License at  \n",
    "  \n",
    "     http://www.apache.org/licenses/LICENSE-2.0  \n",
    "  \n",
    " Unless required by applicable law or agreed to in writing, software  \n",
    " distributed under the License is distributed on an \"AS IS\" BASIS,  \n",
    " WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  \n",
    " See the License for the specific language governing permissions and  \n",
    " limitations under the License."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Only for when running on Colab:\n",
    "import sys\n",
    "if 'google.colab' in sys.modules:\n",
    "    # Get the dependency .py files, if any.\n",
    "    ! git clone https://github.com/GoogleCloudPlatform/cloudml-samples.git\n",
    "    ! cp cloudml-samples/tpu/templates/tpu_triplet_loss_estimator/* .\n",
    "\n",
    "    # Authenticate the user for better GCS access.\n",
    "    # Copy verification code into the text field to continue.\n",
    "    from google.colab import auth\n",
    "    auth.authenticate_user()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import argparse\n",
    "import os\n",
    "import numpy as np\n",
    "import tensorflow as tf\n",
    "from tensorflow.contrib.tensorboard.plugins import projector"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tf.logging.set_verbosity(tf.logging.INFO)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "PREDICT_BATCH_SIZE = 2000\n",
    "EMBEDDING_SIZE = 64"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Triplet loss metric learning with TPU based on https://arxiv.org/abs/1503.03832"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def model_fn(features, labels, mode, params):\n",
    "    # build model\n",
    "    global_step = tf.train.get_global_step()\n",
    "    hidden = tf.layers.dense(features, 100, activation=tf.nn.relu)\n",
    "    outputs = tf.layers.dense(hidden, EMBEDDING_SIZE)\n",
    "\n",
    "    # normalize\n",
    "    embeddings = tf.math.l2_normalize(outputs, axis=1)\n",
    "\n",
    "    # TPUEstimatorSpec.predictions must be dict of Tensors.\n",
    "    predictions = {'embeddings': embeddings}\n",
    "\n",
    "    loss = None\n",
    "    train_op = None\n",
    "\n",
    "    if mode == tf.estimator.ModeKeys.TRAIN:\n",
    "        # define loss\n",
    "        loss = tf.contrib.losses.metric_learning.triplet_semihard_loss(labels, embeddings)\n",
    "\n",
    "        # define train_op\n",
    "        optimizer = tf.train.RMSPropOptimizer(learning_rate=0.05)\n",
    "\n",
    "        # wrapper to make the optimizer work with TPUs\n",
    "        if params['use_tpu']:\n",
    "            optimizer = tf.contrib.tpu.CrossShardOptimizer(optimizer)\n",
    "\n",
    "        train_op = optimizer.minimize(loss, global_step=global_step)\n",
    "\n",
    "    if params['use_tpu']:\n",
    "        # TPU version of EstimatorSpec\n",
    "        return tf.contrib.tpu.TPUEstimatorSpec(\n",
    "            mode=mode,\n",
    "            predictions=predictions,\n",
    "            loss=loss,\n",
    "            train_op=train_op)\n",
    "    else:\n",
    "        return tf.estimator.EstimatorSpec(\n",
    "            mode=mode,\n",
    "            predictions=predictions,\n",
    "            loss=loss,\n",
    "            train_op=train_op)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def train_input_fn(params={}):\n",
    "    mnist = tf.keras.datasets.mnist\n",
    "    (x_train, y_train), _ = mnist.load_data()\n",
    "    x_train = x_train / 255.0\n",
    "\n",
    "    # TPUs currently do not support float64\n",
    "    x_tensor = tf.constant(x_train, dtype=tf.float32)\n",
    "    x_tensor = tf.reshape(x_tensor, (-1, 28*28))\n",
    "\n",
    "    y_tensor = tf.constant(y_train, dtype=tf.int32)\n",
    "\n",
    "    # create tf.data.Dataset\n",
    "    dataset = tf.data.Dataset.from_tensor_slices((x_tensor, y_tensor))\n",
    "\n",
    "    # TPUEstimator passes params when calling input_fn\n",
    "    batch_size = params.get('batch_size', 256)\n",
    "\n",
    "    dataset = dataset.repeat().shuffle(32).batch(batch_size, drop_remainder=True)\n",
    "\n",
    "    # TPUs need to know all dimensions when the graph is built\n",
    "    # Datasets know the batch size only when the graph is run\n",
    "    def set_shapes(features, labels):\n",
    "        features_shape = features.get_shape().merge_with([batch_size, None])\n",
    "        labels_shape = labels.get_shape().merge_with([batch_size])\n",
    "\n",
    "        features.set_shape(features_shape)\n",
    "        labels.set_shape(labels_shape)\n",
    "\n",
    "        return features, labels\n",
    "\n",
    "    dataset = dataset.map(set_shapes)\n",
    "    dataset = dataset.prefetch(tf.contrib.data.AUTOTUNE)\n",
    "\n",
    "    return dataset"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def predict_input_fn(params={}):\n",
    "    batch_size = params.get('predict_batch_size', PREDICT_BATCH_SIZE)\n",
    "\n",
    "    mnist = tf.keras.datasets.mnist\n",
    "    _, (x_test, y_test) = mnist.load_data()\n",
    "    x_test = x_test / 255.0\n",
    "\n",
    "    x_test = x_test[:batch_size]\n",
    "    y_test = y_test[:batch_size]\n",
    "\n",
    "    x_tensor = tf.constant(x_test, dtype=tf.float32)\n",
    "    x_tensor = tf.reshape(x_tensor, (-1, 28*28))\n",
    "\n",
    "    y_tensor = tf.constant(y_test, dtype=tf.int32)\n",
    "\n",
    "    dataset = tf.data.Dataset.from_tensors((x_tensor, y_tensor))\n",
    "\n",
    "    return dataset"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def main(args):\n",
    "    # pass the args as params so the model_fn can use\n",
    "    # the TPU specific args\n",
    "    params = vars(args)\n",
    "\n",
    "    if args.use_tpu:\n",
    "        # additional configs required for using TPUs\n",
    "        tpu_cluster_resolver = tf.contrib.cluster_resolver.TPUClusterResolver(args.tpu)\n",
    "        tpu_config = tf.contrib.tpu.TPUConfig(\n",
    "            num_shards=8, # using Cloud TPU v2-8\n",
    "            iterations_per_loop=args.save_checkpoints_steps)\n",
    "\n",
    "        # use the TPU version of RunConfig\n",
    "        config = tf.contrib.tpu.RunConfig(\n",
    "            cluster=tpu_cluster_resolver,\n",
    "            model_dir=args.model_dir,\n",
    "            tpu_config=tpu_config,\n",
    "            save_checkpoints_steps=args.save_checkpoints_steps,\n",
    "            save_summary_steps=100)\n",
    "\n",
    "        # TPUEstimator\n",
    "        estimator = tf.contrib.tpu.TPUEstimator(\n",
    "            model_fn=model_fn,\n",
    "            config=config,\n",
    "            params=params,\n",
    "            train_batch_size=args.train_batch_size,\n",
    "            # Calling TPUEstimator.predict requires setting predict_bath_size.\n",
    "            predict_batch_size=PREDICT_BATCH_SIZE,\n",
    "            eval_batch_size=32,\n",
    "            export_to_tpu=False)\n",
    "    else:\n",
    "        config = tf.estimator.RunConfig(model_dir=args.model_dir)\n",
    "\n",
    "        estimator = tf.estimator.Estimator(\n",
    "            model_fn,\n",
    "            config=config,\n",
    "            params=params)\n",
    "\n",
    "    estimator.train(train_input_fn, max_steps=args.max_steps)\n",
    "\n",
    "    # After training, apply the learned embedding to the test data and visualize with tensorboard Projector.\n",
    "    embeddings = next(estimator.predict(predict_input_fn, yield_single_examples=False))['embeddings']\n",
    "\n",
    "    # Put the embeddings into a variable to be visualized.\n",
    "    embedding_var = tf.Variable(embeddings, name='test_embeddings')\n",
    "\n",
    "    # Labels do not pass through the estimator.predict call, so we get it separately.\n",
    "    _, (_, labels) = tf.keras.datasets.mnist.load_data()\n",
    "    labels = labels[:PREDICT_BATCH_SIZE]\n",
    "\n",
    "    # Write the metadata file for the projector.\n",
    "    metadata_path = os.path.join(estimator.model_dir, 'metadata.tsv')\n",
    "    with tf.gfile.GFile(metadata_path, 'w') as f:\n",
    "        f.write('index\\tlabel\\n')\n",
    "        for i, label in enumerate(labels):\n",
    "            f.write('{}\\t{}\\n'.format(i, label))\n",
    "\n",
    "    # Configure the projector.\n",
    "    projector_config = projector.ProjectorConfig()\n",
    "    embedding_config = projector_config.embeddings.add()\n",
    "    embedding_config.tensor_name = embedding_var.name\n",
    "\n",
    "    # The metadata_path is relative to the summary_writer's log_dir.\n",
    "    embedding_config.metadata_path = 'metadata.tsv'\n",
    "\n",
    "    summary_writer = tf.summary.FileWriter(estimator.model_dir)\n",
    "\n",
    "    projector.visualize_embeddings(summary_writer, projector_config)\n",
    "\n",
    "    # Start a session to actually write the embeddings into a new checkpoint.\n",
    "    sess = tf.Session()\n",
    "    sess.run(tf.global_variables_initializer())\n",
    "    saver = tf.train.Saver()\n",
    "    saver.save(sess, os.path.join(estimator.model_dir, 'model.ckpt'), args.max_steps+1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "parser = argparse.ArgumentParser()\n",
    "\n",
    "parser.add_argument(\n",
    "    '--model-dir',\n",
    "    type=str,\n",
    "    default='/tmp/tpu-triplet-loss',\n",
    "    help='Location to write checkpoints and summaries to.  Must be a GCS URI when using Cloud TPU.')\n",
    "parser.add_argument(\n",
    "    '--max-steps',\n",
    "    type=int,\n",
    "    default=3000,\n",
    "    help='The total number of steps to train the model.')\n",
    "parser.add_argument(\n",
    "    '--train-batch-size',\n",
    "    type=int,\n",
    "    default=128,\n",
    "    help='The training batch size.  The training batch is divided evenly across the TPU cores.')\n",
    "parser.add_argument(\n",
    "    '--save-checkpoints-steps',\n",
    "    type=int,\n",
    "    default=100,\n",
    "    help='The number of training steps before saving each checkpoint.')\n",
    "parser.add_argument(\n",
    "    '--use-tpu',\n",
    "    action='store_true',\n",
    "    help='Whether to use TPU.')\n",
    "parser.add_argument(\n",
    "    '--tpu',\n",
    "    default=None,\n",
    "    help='The name or GRPC URL of the TPU node.  Leave it as `None` when training on AI Platform.')\n",
    "\n",
    "args, _ = parser.parse_known_args()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# colab.research.google.com specific\n",
    "if 'google.colab' in sys.modules:\n",
    "    import json\n",
    "    import os\n",
    "\n",
    "    # TODO(user): change this\n",
    "    args.model_dir = 'gs://your-gcs-bucket'\n",
    "\n",
    "    # When connected to the TPU runtime\n",
    "    if 'COLAB_TPU_ADDR' in os.environ:\n",
    "        tpu_grpc = 'grpc://{}'.format(os.environ['COLAB_TPU_ADDR'])\n",
    "\n",
    "        args.tpu = tpu_grpc\n",
    "        args.use_tpu = True\n",
    "\n",
    "        # Upload credentials to the TPU\n",
    "        with tf.Session(tpu_grpc) as sess:\n",
    "            data = json.load(open('/content/adc.json'))\n",
    "            tf.contrib.cloud.configure_gcs(sess, credentials=data)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "main(args)"
   ]
  }
 ],
 "metadata": {},
 "nbformat": 4,
 "nbformat_minor": 2
}
